LEGO Horizon Adventures
Fully realised brickbased world, with an action adventure co-op gameplay.

Journey to a distant future, where the land is made of LEGO bricks and lush nature has reclaimed the Earth. Meet the Nora tribe who live in the settlement of Mother’s Heart, and catch your first glimpse of the incredible dinosaur-like machines that roam the forests, mountains, and deserts that stretch beyond the village walls...
Join hunter Aloy as she battles to save Earth from an ancient digital demon, and a gang of sunworshippers who want to live in a world without shade so they can soak up the rays while everything burns.
Hunt machines on your own as Aloy, or unlock colourful heroes Varl, Teersa, and Erend, and use their unique skills to defeat enemies and overcome challenges. Share the fun with another player online**, or via innovative couch co-op on a single screen, so you’re always in the same world together.
Give the village of Mother’s Heart a unique makeover, decorating it in delightful ways to unlock unique LEGO buildings and ornaments. You can even dish out hilarious outfits for your friends to wear!
Uncover secrets from Aloy’s mysterious past as you explore stunning LEGO locations inspired by the world of Horizon, including thriving forests and soaring mountains. Delve deep into the underground Cauldrons or climb the iconic Tallnecks – all beautifully recreated in LEGO bricks.
Need an extra challenge? Replay levels that change to test your skills and unlock new surprises, or check the Mother's Heart Community Board for ways to help the village. See if you've got what it takes to ace every aspect of the game!
The Making of
Having joined Studio GOBO I was immediately put to work. The LEGO title is in my opinion, a great piece of engineering work. Knowing the hoops it had to pass through to release on Switch, PS4, PS5, and PC on the same day! Varying from automatic mesh conversion pipelines, to lighting complexity reduction automation. But let me detail in specific what I spent my time on, from when I joined in the final months up until release.
In short my contributions are as follows:
- General Optimizations, data & content
- CustomDepth occlusion checks
- Performance Macro's
- LOD scaling for DOF
- Nanite loading adjustments
- PSO investigations
-
Multiple Engine integrations:
- VSM Page invalidations
- Volumetric fog reprojection
- ParticleSubUV material expression
- SSS Burley sample optimizations
-
Material & shader debugging to resolve visual issues:
- Specific GPU driver bug due to atomic additions
on the RX7900XTX for example - Materials being incorrectly shaded on XSX
- Specific GPU driver bug due to atomic additions
- Mipmap selection FOV code fixes
- Smaller fixes related to networking and Parametercollection & others
- Debugging viewmodes Switch Light complexity
Detailing all of this is just not worth it. Since I can't give code nor images due to contractual agreements.
So instead I have picked out a few of the above myself that I particularly liked. Which I'll try to explain
to the best of my abilities.
Material & shader debugging:
At a certain point in development we had some issues regarding the Lumen scene.
It was a bit isolated but basically in the nighttime scenes for the game very bright fireflies could appear.
Worse yet if the player moved and thus moved the camera then they could spread uncontrollably!
This of course is less than ideal, so I was placed on the case to determine the source.
We asked Epic if this had happened to them before, since we knew with certainty that
it was due to Lumen. But this came up empty.
Thus we had to dig into it ourselves first. This was done using the helpful tool called Renderdoc.
Which provides a step by step reading of a captured frame. With all the buffers and targets exposed.
Via this it confirmed our suspicions, the origin of the issue was the Lumen probes.
They were in fact filled with NaN's.
How do you determine the cause though? Well, the probes are stored in a rendertarget, handy. This means that we can trace
the pixel's their value's back to the pass of origin via Renderdoc. So, step by step:
for each affected pass I looked in the output to confirm if there was corruption, then checking in the shadercode
where this originates from. Often it gets passed with minor alterations from samples taken from our input targets.
At which point the process repeats for the pixels in those targets. Checking the code is of import thought,
since some targets are persistent across frames if you aren't careful you end up in a dead end or a loop.
In the end I kept repeating the process until the values came from a buffer. Which is originally filled using
functions like InterlockedAdd. Confirming the validity of the code on other platforms proved that
in the end it was a driver issue. Which was circumventable by reordering the code.
CustomDepth occlusion checks:
Our camera is only indirectly controlled by the player and is placed at a somewhat isometric angle.
In practice this means that the player can sometimes be partially or even completely hidden from the user's eyes.
How to resolve this? Custom depth! We can render the player's geometry and other characters as well for that matter
into a separate target. Which in postprocesses can be integrated into the final images. This is however a well-known technique.
In our case we didn't want it to be omnipresent, for aesthetic reason. since it is quite distracting if it appears
even when only a small part of the character is covered (it would of course only shade those pixels). No, we wanted
that if a certain percentage of our character got covered that only then it would activate.
We used occlusion queries which gives us an idea of the number of pixels that would potentially get rendered.
This isn't directly comparable against the actual amount shaded. Since for occlusion queries the engine smartly uses
bounding boxes. To not make it too difficult on ourselves we applied a constant product to account for this difference.
Which gets scaled based on the bounding box's axis' lengths that are least aligned with the view. IE when looking from the top
we assume the X/Y axes are determinant on the rough surface area that will get covered. Or when a cube the constant is not
scaled.
Nanite loading adjustments:
Nanite has the tricky issue that in return for highly detailed micro-geometry we can't hold all that data simultaneously
in VRAM. So the engine has to cache the pages it needs, which can take some time as they must be fetched from "disk".
This can be mitigated by increasing the minimum residency, set by default to 32kb. This scales how much mesh data HAS
to be loaded in before Nanite tries to rendering the geometry.
This issue of course isn't very common. Rather it mainly appears when you have 1 very large mesh or you have a highly detailed scene.
And then in particular, when you teleport the camera, or when a level is loaded/geometry is revealed. At which point you will have
what can only be described as traditional LOD popping. Not ideal in a world made out of LEGO's. As any wonky geometry is
quickly noticed.
There isn't a perfect solution for this. If you have the option then like mentioned you can increase
the minimum residency. Granted that enough free VRAM is available. If not then you will need to add the following code.
Basically, you can request Nanite to preload/fetch specific meshes. This is done via the render thread but likely will get
delegated from the gamethread. That's all good and well but there is no way for you to know yet if the mesh has been loaded in.
For this I passed along a pointer to a bound delegate. Which gets stored together with the request but in separate array.
Then when a certain required amount of residency gets achieved the system broadcasts the delegate (back over to the gamethread).